#!/usr/lib/python

class SaploError(Exception):
	"""
	Is thrown when an request to the Saplo API for some reason fails

	All requests to the SaploJSOnClient should catch this exception, and handle it
	"""
	def __init__(self, value):
		self.value = value
	def __str__(self):
		return repr(self.value)


import json,urllib,urllib2
class SaploJSONClient:
	"""
	 Saplo JSON Client.
	 Handles authentication and json-requests to the saplo API Server.

	 All requests to the SaploJSONClient should catch the SaploError that is thrown if a request fails

	 Example of request usage: All these requests returns a dictionary that you can use to retrieve data
	 try:
	 	client = SaploJSONClient()
	 	client.createCorpus("My new Corpus", "Some description text", "sv")
	 	client.addArticle(corpusId, TitleString, LeadString, BodyString, Date, "someurl.com", "some author")
		client.getEntityTags(corpusId, articleId, waittime)
	 	client.getSimilarArticles(corpusId,articleId,(id1,id2), waittime)
	 	client.getCorpusPermission()
	except SaploError, err:
		print err.__str__()
	"""
	url       = "http://api.saplo.com/rpc/json;jsessionid={token}"
	apikey    = ''
	secretkey = ''
	token = ""
	errorCodes = {  
			501:      "No matching results",
			592: 	  "Undocumented error",
			899: 	  "Unknown error",
			1001:     "Missing required field(s).",
			1002:     "Sorry, this language is not yet supported",
			1003:     "Processing request.",
			1004:     "Sorry, still processing but I'm working as fast as I can.",
			1005:     "Sorry, your max waiting time was reached, but I'm still processing.",
			1006:     "No results was found.",
			1101:     "Username or Password incorrect",
			1201:     "No permission for that operation on the corpus",
			1202:     "No corpus exists for user, need to create corpus first",
			1203:     "Max number of corpus exceeded",
			1204:     "No article exists with that id in that corpus",
			1205:     "Article already exists",
			1206:     "Article language and corpus language doesn't match.",
			1301:     "Couldn't find any tags for this article    ",
			1302:     "Couldn't find any tag with that id    ",
			1303:     "No blacklist exists",
			1304:     "Blackword already exists",
			1501:     "No matching results was found",
			1502:     "Couldn't find any match with that id",
			1401:     "No context exists",
			1402:     "No permission for that operation on the context"
			}
	def __init__(self,apikey, secretkey):
		"""
		Initiates the Saplo JSONClient using the secret & api keys
		@type String
		@param Saplo API key
		@type String
		@param Saplo Secret key
		"""
		self.apikey    = apikey
		self.secretkey = secretkey

		self.__createSession(self.apikey, self.secretkey)
	def getArticle(self,corpusId, articleId):
		"""
		Gives information about the saved headline and publish url for a specific article.
		@type number
		@param corpusId - The unique id of the corpus where the article is stored
		@type number
		@param articleId - the id for the article you want information about
		@rtype dictionary
		@return 
			corpusId   Number - The id of the corpus that the article exists in
			articleId  String - The id for the article you requested
			headline   String - The headline for the article
			publishUrl String - The url (if it exists) that are saved for the article
			
		"""
		params = (corpusId, articleId)
		response = self.__doRequest('corpus.getArticle', params)
		return self.__handleJSONResponse(response.read())
	def getEntityTags(self,corpusId, articleId, waiton):
		"""
		Gives you the persons, organisations and geographical names (places) that are mentioned within an article/text.
		@type number
		@param corpusId  - The unique id for the corpus where the source article is stored.
		@param articleId - The id for the article to which you want to find similar articles.
		@param waitOn    - This param specifies how long you want to wait for a result to be calculated or if you want to just start a search
			and come back later to fetch the result. 
			We RECOMMEND you to use waitOn = 0. On high load the calculation might not be able to process all requests and then its better 
			to add a search to the queue and fetch the result
		@rtype dictionary
		@return
			tagId	  Number - an id for the result based on your articleId and corpusId. (corpusId + articleId + tagId are a unique combination)
			tagWord	  String - a word that has been recognized as an entity and is represented in the article/text
			tagTypeId Number - specifies which category a tag has been placed in. 3 = person, 4 = organisation, 5 = geoname (place) 
			
		"""
		params = (corpusId, articleId,waiton)
		response = self.__doRequest('tags.getEntityTags', params)
		return self.__handleJSONResponse(response.read())
	def getSimilarArticles(self,sourceCorpusId, sourceArticleId,againstCorpusIds, waiton):
		"""
		Searches the corpus you provide and looking for articles that has a similar semantic meaning as your source article. 
		The request gives you a list with a maximum of 50 articles that are similar to the source article.
		
		@type Number
		@param sourceCorpusId   - The unique id for the corpus where the source article is stored.
		@type Number
		@param sourceArticleId  - The id for the article to which you want to find similar articles.
		@type Array
		@param againstCorpusIds - An array containing all corpus ids where you want to search for similar articles
		@type Number
		@param waitOn           - This param specifies if you want to wait until a result has been calculated or
		if you want to just start a search and come back later to fetch the result. We RECOMMEND you to use waitOn = 0. 
		On high load the calculation might not be able to process all requests and then its better to add a search to the queue and fetch the result later. 
	
		@rtype dictionary
		@return
			matchId            Number        an id for the result based on your sourceCorpusId and sourceArticleId.
				(sourceCorpusId + sourceArticleId + matchId are a unique combination)
			resultCorpusId     Number        id for the corpus where the similar article exists
			resultArticleId    Number        id for the similar article
			resultValue        Number    value of how similar the result article is.
				The scale goes from 0.00 to 1.00. A result of 1 equals the exact same article.
		
		"""
		#Json-rpc-java compatible list
		javarpcList = {'javaClass':"java.util.ArrayList",
				'list':againstCorpusIds}

		params = [sourceCorpusId, sourceArticleId,javarpcList,waiton]
		response = self.__doRequest('match.getSimilarArticles', params)
		return self.__handleJSONResponse(response.read())
	def createCorpus(self,corpusName, corpusDesc, lang):
		"""
		Creates a new corpus (container to store articles and texts in) and returns an id to the created corpus. 
		A corpus may only contain articles with the same language (i.e. only english texts in the corpus).

		@type String
		@param corpusName - Provide a name for your new corpus.
		@type String
		@param corpusDesc - Provide a description for your new corpus. 
			Use something that describes what the corpus contains (i.e. English blog posts from my personal blog)
		@type String
		@param lang - Specify what language the articles/texts that will be stored in the corpus are written in.
			English and Swedish are supported and specified according to ISO 639-1 (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). 
			Swedish = "sv", English = "en".
		
		@rtype dictionary
		@return 
			corpusId    Int        A unique id for your newly created corpus.
		"""
		params = (corpusName,corpusDesc,lang)
		response = self.__doRequest('corpus.createCorpus', params)
		return self.__handleJSONResponse(response.read())
	def addArticle(self,corpusId, headline, lead, body, publishStart, publishUrl, authors):
		''' 
		Add a new article to a corpus. 
		If you are adding an article that already exists (the combination of headline+body+publishUrl) then the id for that article will be returned.

		@type  Number
		@param corpusId (required) - The id to the corpus where you want to add your article.
		@type  String
		@param headline (required) - The article headline
		@type  String
		@param lead      	   - The article lead text
		@type  String
		@param body     (required) - The body text for the article
		@type  date
		@param publishDate         - The date when the article was published (YYYY-MM-DD HH:MM:SS, i.e. 2010-01-24 12:23:44)
		@type  String
		@param publishUrl          - The url for where the article can be found on internet
		@type  String
		@param authors             - The authors of the article or text
		@type  String
		@param lang     (required) - The language for the article. 
			English and Swedish are supported and specified according to ISO 639-1 (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). 
			Swedish = "sv", English = "en".
		@rtype dictionary
		@return 
			corpusId    Number        A unique corpus id.
			articleId   Number        The id for the new article.
		'''
		params = (corpusId, headline, lead, body, publishStart, publishUrl, authors,'sv')
		response = self.__doRequest('corpus.addArticle', params)
		return self.__handleJSONResponse(response.read())
	def getCorpusPermission(self):
		"""
		Gives you a list to all corpus ids that you have read or write permission to.

		@rtype Dictionary
		@return 
			corpusId    Int        A unique corpus id.
			permission    String    The permission you have to the unique corpusId. This can have the values of "read" or "write"
		"""
		response = self.__doRequest('corpus.getPermissions',() )
		return self.__handleJSONResponse(response.read())
	def getCorpusInfo(self,corpusId):
		"""
		Gives you the name, description and the last article id for a specific corpus.

		@type  Number
		@param corpusId The unique id for the corpus you want information about.
		@rtype Dictionary
		@return 
			corpusId        Int        The unique id for the corpus you want information about.
			corpusName      String    The current name for the corpus
			corpusDesc      String    The current description for the corpus
			lang            String    The language that are specified for this corpus
			lastArticleId   Int    The id for the last article that has been added to the corpus.
		"""
		params = [corpusId]
		response = self.__doRequest('corpus.getInfo',params)
		return self.__handleJSONResponse(response.read())
	def __createSession(self,apiKey, secretKey):
		"""
		Creates a session towards the Saplo API

		@type String
		@param apikey - The apikey to access the Saplo API
		@type String 
		@param secretkey - The secret key to access the Saplo API
		"""
		#Request a new session
		response = self.__doRequest('auth.createSession',(apiKey,secretKey))
		
		# Get the response
		jsonresponse = response.read()
		#If our request fails, raise an SaploException
		try:
			self.__handleJSONResponse(jsonresponse)
		except SaploError, err:
			raise err
		#Decode the JSON request and retrieve the token,establishing it as our given token
		result = json.loads(jsonresponse)
		token  = result['result']
		self.__setTokenTo(token)
	def __doRequest(self, meth, param,sapid=0):
		'''
		Creates an JSON request to the server from the params
		'''
		#HTTP params
		options = json.dumps(dict(
			method = meth,
			params = param,
			id=sapid))
		#Parse the url-string to contain our session-token
		url = self.url.format(token = self.token)

		#Create HTTP request
		request  = urllib2.Request(url,options)
		response = urllib2.urlopen(request)
		return response
	def __setTokenTo(self, t):
		'''
		Sets the class token string to the given param
		'''
		self.token = t;
	def __handleJSONResponse(self, jsonresponse):
		response = json.loads(jsonresponse)
		#If errors, handle them
		if "error" in response:
			errormsg  =  "" if ('msg'  not in response['error']) else response['error']['msg'] 
			errorcode =  "" if ('code' not in response['error']) else response['error']['code'] 			

			errorcodeStr = ""
			if errorcode in self.errorCodes.keys():
				errorcodeStr = self.errorCodes[errorcode]
			else:
				errorcodeStr = " Unknown error"
			#Create a readable error message
			msg = "An error has occured: '{errormessage}' With code = ({errorcode}) {errorcodeString}".format(
					errormessage = errormsg,
					errorcode    = errorcode,
					errorcodeString    = errorcodeStr

					);
			#Raise an SaploError
			raise SaploError(msg)
		##Otherwise we have a sucessfull response
		return response

